package com.sfzd5.exam;

import android.graphics.Bitmap;

import com.sfzd5.exam.helplibrary.exam.PageA4;

import org.opencv.android.Utils;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class PageProcess {
    public static final double PageRatio = 2;
    private static final double MarkSize = 5;
    private static final double MarkBoard = 1;

    double PageWidthRatio;

    //灰度原图
    public Mat src;
    public Mat cameraAreaMat;
    public Mat binMat;
    public Mat cameraPageMat;
    public Mat pageMat;

    double cameraWidth;
    double cameraHeight;
    int grayType;
    double pageWidth;
    double pageHeight;
    double blockWidth = PageA4.mm2point(5) * PageRatio;

    public PageProcess(Bitmap bitmap, double cameraWidth, double cameraHeight, double pageWidthRatio){

        this.cameraWidth = cameraWidth;
        this.cameraHeight = cameraHeight;
        this.PageWidthRatio = pageWidthRatio;
        pageWidth = PageA4.getPageWidth() * PageRatio;
        pageHeight = PageA4.getPageHeight() * PageRatio;

        Mat rgbMat = new Mat();
        Utils.bitmapToMat(bitmap, rgbMat); //convert original bitmap to Mat, R G B.
        //Mat rgbMat = bitmap.toMat();

        src = OpenCVHelper.gray(rgbMat);
        grayType = src.type();
    }

    public Mat takeMat(){
        PageContentArea p =OpenCVHelper.takePageContentArea(binMat, createTemplate());
        pageMat = OpenCVHelper.takePage(binMat, p);
        cameraPageMat = OpenCVHelper.takePage(cameraAreaMat, p);
        return pageMat;
    }

    public List<Mat> getCheckedInfo(Mat curMat) {

        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;

        int w = curMat.cols();
        int h = curMat.rows();
        double wm = PageA4.contextWidthm() + 1.5;
        int px = 3;
        List<List<Mat>> lineOptions = new ArrayList<>();
        List<List<Integer>> lineOptionsBlackCount = new ArrayList<>();

        for(int i=0; i<4; i++){
            int p = px + 44 * i + 6; //数字
            List<Mat> questionOptions = new ArrayList<>();
            List<Integer> questionBlankCount = new ArrayList<>();
            for(int j =0; j<4; j++) {
                Rect rect = new Rect((int)(w/wm*p), 0, (int)(w/wm*7), h);
                Mat m = new Mat(curMat, rect);
                int count = OpenCVHelper.countBlack(m);
                if(count>max) max = count;
                if(count<min) min = count;
                questionOptions.add(m);
                questionBlankCount.add(count);
                p+=7;
            }
            lineOptions.add(questionOptions);
            lineOptionsBlackCount.add(questionBlankCount);
        }
        //平均黑点数
        int pj = (max+min)/2;

        StringBuilder sb = new StringBuilder();
        for(List<Integer> options : lineOptionsBlackCount){
            for(int oc : options){
                if(oc>pj)
                    sb.append("x");
                else
                    sb.append("_");
            }
            sb.append(";");
        }

        List<Mat> mats=new ArrayList<>();
        for(List<Mat> options : lineOptions) {
            for (Mat oc : options) {
                mats.add(oc);
            }
        }

        return mats;

    }

    public String getCheckedInfo2(Mat curMat) {

        int blockSize = curMat.rows(); //blockSize应与四边的方块相同，或与选择题
        if(blockSize%2==0) blockSize++;
        int C = 10; // 应该能达到去除杂点的功能
        Mat dst = new Mat();
        Imgproc.adaptiveThreshold(curMat, dst, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, blockSize, C);

        List<List<Mat>> lineOptions = OpenCVHelper.cutOption(dst);
        List<List<Integer>> lineOptionsBlackCount = new ArrayList<>();
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for(List<Mat> options : lineOptions){
            List<Integer> optionsBlackCount = new ArrayList<>();
            for(Mat mat : options){
                int count = OpenCVHelper.countBlack(mat);
                if(count>max) max = count;
                if(count<min) min = count;
                optionsBlackCount.add(count);
            }
            lineOptionsBlackCount.add(optionsBlackCount);
        }

        //平均黑点数
        int pj = (max+min)/2;

        StringBuilder sb = new StringBuilder();
        for(List<Integer> options : lineOptionsBlackCount){
            for(int oc : options){
                if(oc>pj)
                    sb.append("x");
                else
                    sb.append("_");
            }
            sb.append(";");
        }

        return sb.toString();
    }

    public List<Mat> findQuestionArea() {
        //原图
        Mat yt = pageMat.clone();

        Mat dst = pageMat.clone();
        /**
         边缘检测, threshold1  threshold1
         低于阈值1的像素点会被认为不是边缘；
         高于阈值2的像素点会被认为是边缘；
         在阈值1和阈值2之间的像素点,若与第2步得到的边缘像素点相邻，则被认为是边缘，否则被认为不是边缘。
         */
        Imgproc.Canny(pageMat, dst, 60, 200);

        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        // 寻找轮廓，之前必须进行边缘检测
        /**
         * 第一个参数：image，单通道图像矩阵，可以是灰度图，但更常用的是二值图像，一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像；
         * 第二个参数：contours，定义为“vector<vector<Point>> contours”，是一个向量，并且是一个双重向量，向量内每个元素保存了一组由连续的Point点构成的点的集合的向量，每一组Point点集就是一个轮廓。有多少轮廓，向量contours就有多少元素。
         * 第三个参数：hierarchy，定义为“vector<Vec4i> hierarchy”，先来看一下Vec4i的定义
         *                            typedef    Vec<int, 4>   Vec4i;
         *            Vec4i是Vec<int,4>的别名，定义了一个“向量内每一个元素包含了4个int型变量”的向量。
         *            所以从定义上看，hierarchy也是一个向量，向量内每个元素保存了一个包含4个int整型的数组。
         *            向量hiararchy内的元素和轮廓向量contours内的元素是一一对应的，向量的容量相同。
         *            hierarchy向量内每一个元素的4个int型变量——hierarchy[i][0] ~hierarchy[i][3]，分别表示第
         *         i个轮廓的后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号。如果当前轮廓没有对应的后一个
         *         轮廓、前一个轮廓、父轮廓或内嵌轮廓的话，则hierarchy[i][0] ~hierarchy[i][3]的相应位被设置为
         *         默认值-1。
         *
         *
         * 第四个参数：int型的mode，定义轮廓的检索模式：
         *            取值一：CV_RETR_EXTERNAL只检测最外围轮廓，包含在外围轮廓内的内围轮廓被忽略
         *            取值二：CV_RETR_LIST   检测所有的轮廓，包括内围、外围轮廓，但是检测到的轮廓不建立等级关
         *                   系，彼此之间独立，没有等级关系，这就意味着这个检索模式下不存在父轮廓或内嵌轮廓，
         *                   所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1，具体下文会讲到
         *            取值三：CV_RETR_CCOMP  检测所有的轮廓，但所有轮廓只建立两个等级关系，外围为顶层，若外围
         *                   内的内围轮廓还包含了其他的轮廓信息，则内围内的所有轮廓均归属于顶层
         *            取值四：CV_RETR_TREE， 检测所有轮廓，所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓，内
         *                    层轮廓还可以继续包含内嵌轮廓。
         *
         * 第五个参数：int型的method，定义轮廓的近似方法：
         *            取值一：CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
         *            取值二：CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息，把所有轮廓拐点处的点保存入contours
         *                    向量内，拐点与拐点之间直线段上的信息点不予保留
         *            取值三和四：CV_CHAIN_APPROX_TC89_L1，CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
         *
         * 第六个参数：Point偏移量，所有的轮廓信息相对于原始图像对应点的偏移量，相当于在每一个检测出的轮廓点上加
         *             上该偏移量，并且Point还可以是负值！
         */
        Imgproc.findContours(dst, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));

        // 找出匹配到的最大轮廓
        List<RotatedRect> areaRects = new ArrayList<>();
        int minArea = dst.cols() * dst.rows() / 30;
        // 找出匹配到的最大轮廓
        for (int i = 0; i < contours.size(); i++) {
            MatOfPoint matOfPoint = contours.get(i);
            double tempArea = Imgproc.boundingRect(matOfPoint).area();
            if (tempArea > minArea) {
                MatOfPoint2f matOfPoint2f = new MatOfPoint2f(matOfPoint.toArray());
                RotatedRect rect = Imgproc.minAreaRect(matOfPoint2f);
                areaRects.add(rect);
            }
        }

        Collections.sort(areaRects, new Comparator<RotatedRect>() {
            @Override
            public int compare(RotatedRect o1, RotatedRect o2) {
                return Double.compare(o1.center.y, o2.center.y);
            }
        });

        List<Mat> result = new ArrayList<>();
        for(int i=0; i<areaRects.size(); i++) {
            RotatedRect rect = areaRects.get(i);
            Mat correctImg = OpenCVHelper.rotatedRectPerspective(yt, rect);
            result.add(correctImg);
        }
        return result;
    }

    /**
     * 二值化
     * @return
     */
    public Mat threshold(){
        binMat = OpenCVHelper.threshold(cameraAreaMat, blockWidth, 10);
        return binMat;
    }

    public Mat cutPage(){
        double cPageWidth = pageWidth/PageWidthRatio;
        double cPageHeight = pageHeight/PageWidthRatio;

        double srcWidth = src.cols();
        double srcHeight = src.rows();

        //因为上下各有一部分多余图片，求出去除和这一部分的有效区域
        double areaWidht = cameraWidth;
        double areaHeight =  areaWidht * cPageHeight / cPageWidth;

        //图片大小与预览大小的比例
        double bl = srcHeight / cameraHeight;
        double srcAreaWidht = areaWidht * bl;
        double srcAreaHeight = areaHeight * bl;

        //源图中要截取的坐标
        double x = (srcWidth - srcAreaWidht)/2;
        double y = (srcHeight - srcAreaHeight)/2;

        //要截取的源图的四个坐标
        Point p1 = new Point(x, y);
        Point p2 = new Point(x+srcAreaWidht, y);;
        Point p3 = new Point(x, y+srcAreaHeight);
        Point p4 = new Point(x+srcAreaWidht, y+srcAreaHeight);

        cameraAreaMat = OpenCVHelper.Perspective(src, p1, p2, p3,p4, cPageWidth, cPageHeight);
        return cameraAreaMat;
    }

    private Mat createTemplate(){
        //每毫米点数
        double templateWidth = PageA4.mm2point(MarkSize + MarkBoard * 2) * PageRatio;
        double mm1 = PageA4.mm2point(MarkBoard) * PageRatio;

        //灰度图
        Mat template = new Mat((int)templateWidth, (int)templateWidth, grayType, new Scalar(255,255,255));
        Point p1 = new Point(mm1, mm1);
        Point p2 = new Point(mm1+blockWidth, mm1+blockWidth);
        OpenCVHelper.fillRectangle(template, p1, p2, new Scalar(0,0, 0));

        return template;
    }

}
